6 - pandas, filtrering, logikk og betingelser
Noe av det vi gjør oftest når vi programmerer er å teste om en betingelse er sann eller usann. Når testen er utført, kan programmet velge riktig vei å gå i fortsettelsen. Slik testing kalles "logikk". Vi har allerede i innledningen vært inne på variabeltypen som avgjør om en test er sann eller usann, typen bool
.
Sann eller usann (True
og False
) brukes som oftest sammen med en if
-betingelse, slik som dette:
if False:
print("Hei")
else:
print("Hå")
Vanligvis vil if
-setningen inneholde en test som ikke er helt åpenbar, og som avhenger av input i en funksjon eller en variabel som er definert et annet sted i koden. Her er et eksempel på det første:
import numpy as np
def did_I_win(s0,s1):
if s1>s0:
return "won!"
else:
return "lost"
PricePaid=100
StockPriceToday=int(np.random.rand()*200)
print(
f"Bought for: {PricePaid}\n"
f"Worth today: {StockPriceToday}\n"
f"You {did_I_win(PricePaid,StockPriceToday)}"
)
Ofte vil du ønske å sammenligne ulike datasett, for eksempel to numpy-rekker. Dersom du skal ha en if
-betingelse, må du imidlertid huske på at for å få ett svar på en sammenligning mellom to numpy-rekker, så må du angi på hvilken måte de skal sammenlignes. I utgangspunktet vil en slik sammenligning bare gi resultatet fra en sammenligning av hvert element i de to rekkene. Resultatet av sammenligningen er altså ikke en bolsk verdi (bool
) men en ny rekke. En slik sammenligning vil derfor føre til en feil:
a=np.array([1,2,3])
b=np.array([3,2,1])
print(a>b)
if a>b:
print('a was bigger than b')
I stedet må vi angi hvordan den elementvise sammenligningen skal sammenfattes i et sant/usant-resultat. Vi kan da velge mellom å kreve at alle elementene er sanne med numpyfunksjonen all()
, eller at minst ett av elementene er sanne medany()
.
if np.any(a>b):
print('There were elements in a where the corresponding element in b was smaller')
a=np.array([101,102,103])
if np.all(a>b):
print('All elements in a were bigger than the corresponding element in b')
Ofte vil du ha bruk for å bruke flere vilkår for testen, der du enten krever at alle må være sanne, eller at det holder at én er sann, eller en kombinasjon. Det oppnår vi med and
og or
.
Et annet nyttig nøkkelord for logiske tester er in
. Dette brukes på alt fra å finne ut om et objekt er i en liste eller annen datastruktur, til om en delstreng er i en streng. Her er et eksempel som bruker alle disse nøkkelordene i if
og elif
-setninger:
animal='lion'
cats=['lion','tiger','puma','lynx']
mammals=['human','bear','cat','whale','mouse']+cats
if (animal in cats) and (animal in mammals):
print(f"{animal} is a cat and a mammal")
elif (not animal in cats) and (animal in mammals):
print(f"{animal} is not a cat, but it is a mammal")
elif (animal in cats) and (not animal in mammals):
print(f"{animal} is a cat, but not a mammal")
else:
print(f"{animal} is neither a cat nor a mammal")
Legg merke til elif
over. De er if
-setninger som er knyttet til if
-setningen over. Koden til en elif
-setning kjøres dersom hverken if
-setningen eller noen elif
-setninger over har blitt tilfredsstilt.
Vanligvis vil slike tester ligge inne i kode der det ikke er helt åpenbart hva variabelen du tester er. Dette er illustrert i eksemplet under, der variabelen bestemmes av innholdet i datasettet "schooling-gdp.csv". Vi starter med å hente inn datasettet og plotte BNP per capita mot Utdanning:
import pandas as pd
df=pd.read_csv('./data/schooling-gdp.csv')
df.plot.scatter('BNP_per_capita','Utdanning')
Vi ser at sammenhengen virker mer usikker for når utdanningsnivået og BNP per capita er høyt. Vi kan derfor bruke en if
-setning til å kun ta med land med BNP per capita høyere enn 20000. Vi kan da kjøre en løkke som enten legger til True
eller False
i en liste include
som identifiserer de radene i df
som vi ønsker å ta med.
Når vi har denne listen, kan vi plukke de radene med True
ved å sette listen i klammeparentes etter df
, altså df[include]
:
include = []
for index, (country,code,gdp_cap,edu,pop) in df.iterrows():
if gdp_cap>20000:
include.append(True)
else:
include.append(False)
df_filtered=df[include]
df_filtered.plot.scatter('BNP_per_capita','Utdanning')
Som vi ser, ser det ut til å være er det liten sammenheng mellom BNP per capita og utdanningsnivå for disse observasjonene.
Det finnes imidlertid en enklere måte å velge ut variabler i pandas. Du kan rett og slett sette betingelsen rett i klammeparentesen ved å referere til datarammen og den variablen det gjelder. Det vil for eksempel si df[df['BNP_per_capita']>20000]
. Koden blir da en god del enklere:
df[df['BNP_per_capita']>20000].plot.scatter('BNP_per_capita','Utdanning')
I eksemplet over leser Pandas dataene fra en fiil, men Pandas kan også hente data direkte fra en database på nettet. Vi skal først se hvordan vi kan hente data fra titlon.uit.no
. Dette er en database som inneholder informasjon om aksjer, derviater og obligasjoner handlet på Oslo Børs Euronext.
Det første vi må gjøre er å hente en pakke som kan kommunisere med serveren. Vi skal bruke pymysql. Den er i utgangspunktet ikke installert her på uit's jupyter-server, så det må vi gjøre selv. Det gjør du slik:
Åpne konsollen og skriv pip install pymysql
For å hente data gjør du følgende:
På https://titlon.uit.no/ ligger det børsinformasjon fra Oslo Børs. For å hente data fra Titlon gjør du følgende:
LES INSTRUKSJONENE OVER! KODEN UNDER KAN IKKE KJØRES UTEN VIDERE.
import pandas as pd
#Query script for MySQL client
import pymysql
con = pymysql.connect(host='titlon.uit.no',
user="user@domain.no",
password="W4wuZGUaJ!wxvxrZd5%AF",
database='OSE')
crsr=con.cursor()
crsr.execute("""
SELECT * FROM `OSE`.`equity`
WHERE (`ISIN` = 'NO0010279474')
AND year(`Date`) >= 2000
ORDER BY `Name`,`Date`
""")
r=crsr.fetchall()
df=pd.DataFrame(list(r), columns=[i[0] for i in crsr.description])
df
Koden leser inn dataene til en pandas dataframe
, eller dataramme. Denne er kalt df
her. Om vi ser nærmere på koden, så ser vi at den gjør følgende:
lager et objekt con
som representerer forbindelsen med databasen. Denne lages ved å bruke connect()
-funksjonen til pymysql
, som er en pakke for å hente data fra MySQL-databaser. Input er navn på server, brukernavn, passord (automatisk generert) og navn på database.
lager en streng sql
med kommandoen til databasen, i SQL-språk
dataframe
som kalles df
ved å bruke read_sql_query
-funksjonen i pandasFør vi fortsetter er det kanskje greit å forklare hva SQL er. SQL er et språk laget spesifikt for å hente ut data fra dataabaser. Nesten alle databaser er SQL-basert. Syntaksen kan virke litt knotete, men kan være lurt å lære seg dette språket. En god SQL-spørring kan spare deg for mye programmering senere. Dette er imidlertid ikke et kurs i SQL, så vi går ikke nærmere inn på denne syntaksen her.
I tabellen vi henter, over, mangler endel variabler fordi det ikke er plass. For å se navnet på alle variablene kan du bruke keys()
-funksjonen til datarammen:
df.keys()
En dataframe ligner litt på en oppslagsliste. Du henter frem variabelen du vil bruke ved hjelp av hakeparenteser. I Titlondatabasen er avkastningen lnDeltaP
, så la oss ta en kikk på på den:
df['lnDeltaP']
Det viser seg at i aksjemarkedet, så har aksjene en tendens til å bevege seg i samme retning. Går børsen først ned, så vil det gjelde de aller fleste aksjene på børsen. Gjennomsnittetlig avkastning for alle børsens aksjer måles av børsindeksen. I Titlon er børsindeksen lagt inn som en variabel for hvert selskap under navnet lnDeltaOSEBX
. La oss derfor undersøke om det stemmer at det er en nær sammenheng mellom Equinors kurs og børsindeksen, ved hjelp av matplotlib
:
df.plot.scatter('lnDeltaP','lnDeltaOSEBX')
Du kan finne informasjon om andre aksjer enn Equinor om du vil. Koden under kjører en SQL-spørring som henter navnene på alle selskap i databasen etter 2018. Om du vil hente ut et annet selskap, kan du velge blant disse
sql="SELECT distinct `Name`,`ISIN`,`SecurityId`,CompanyId FROM equity WHERE Year(`Date`)>2018 ORDER BY `Name`"
companies = pd.read_sql_query(sql, con)
companies
La oss se nærmere på tre av selskapene, og plott dem sammen. Vi bruker en SQL WHERE
-betingelse til å begrense utvalget til de tre selskapene som er nevnt og til datoer etter 2000. Så tar vi også med i SQL-setningen en sortering på dato:
sql=("SELECT distinct Name,date, AdjustedPrice FROM equity "
"WHERE (Name='Equinor' OR Name='Tomra Systems' OR Name='DNB') "
"AND `Date`>'2000-01-01'"
"ORDER BY `Date`")
Vi kan nå kjøre denne spørringen mot Titlon:
df=pd.read_sql_query(sql, con)
df
Dataene ligger nå i "lengdeformat". Det vil si at selskapene ligger etterhverandre i en lang tabell. Om vi ønsker å sammenligne utviklingen kan det være en fordel å få prisene i hvert selskap i en egen kolonne. Det gjør vi slik:
df=df.pivot(index='date', columns='Name', values='AdjustedPrice')
df
Equinor ble imidlertid ikke børsnotert før i 2001. I alle observasjoner før børsnoteringen står det derfor NaN
, hvilket betyr "ingen observasjon". For å kunne sammenligne må alle tre aksjene være observert på hver dato. Vi kan oppnå dette med funksjonen dropna()
:
df=df.dropna()
df
Vi ser nå at de tomme cellene er borte. Equinor ble altså børsnotert 18. juni 2001.
For å kunne sammenligne selskapenes utvikling bedre, bør de starte på samme tidspunkt. For å gjøre det deler vi alle observasjonene på den første. For å dele observasjoner på noe, bruker vi div()
-funksjonen til datarammen. Vi skal dele på første rad, som er df.iloc[0]
. Så multipliserer vi det hele med 100:
df=100*df.div(df.iloc[0])
df
Vi ser nå at alle aksjene starter på 100 den datoen Equinor ble børsnotert. Hvilet selskap har vært det beste å investere i? Hvilket har vært verst?
Vi kan nå plotte utviklingen. Når vi jobber med pandas, er det best å bruke den innebygde plot()
-funksjonen i pakken. Da får vi automatisk riktige etiketter:
ax=df.plot()
ax.legend(loc='upper left',frameon=False)
Eurostat er en veldig nyttig kilde til data. De har et stort utvalg av data på europeiske land. De har laget en egen pakke til python som ikke overraskende heter eurostat
. Denne pakken er heller ikke installert i utgangspunktet på jupyter.uit.no. Dere må derfor åpne "Terminal" og kjøre pip install eurostat
for å kjøre den.
For å se hvilke tabeller som er der, henter vi innholdsfortegnelsen (table of contents) med funksjonen get_toc_df
. Det gir oss en pandas
dataramme:
import eurostat
toc = eurostat.get_toc_df()
toc
Du kan finne hvor mange tabeller det er med len(toc)
, og det er i skrivende stund over ti tusen tabeller. Det blir litt krevende å lete gjennom alle, så vi ønsker derfor å begrense antall tabeller til de som er relevante. Anta at vi ønsker å se på utviklingen i BNP per innbygger for landene som er med i statistikken. Nærmere bestemt, vi ønsker å finne alle tabeller der 'GDP per capita'
er i tittelen.
BNP per innbygger er hvor mye hver person i landet i gjennomsnitt produserer i løpet av ett år.
Vi gjør dette ved å lage følgende "list comprehension": ['GDP per capita' in i for i in toc['title']]
. Elementene i denne listen er sant (True
) dersom 'GDP per capita' er i tittelen, og usant (false
) om det ikke er i tittelen.
gdp_in_toc_list=['GDP per capita' in i for i in toc['title']]
Vi kan så velge de elementene hvor betingelsen er sann ved å sette listen inn i en klammeparentes etter innholdsfortegnelsen, toc[gdp_in_toc_list]
. Kun de elementene i innholdsfortegnelsen der det står sant (True
) i listen, tas da med.
#collecting only the tables where 'GDP per capita' is in the name
toc_gdp=toc[gdp_in_toc_list]
toc_gdp
La oss nå se på tabellen "Purchasing power adjusted GDP per capita", som har kode 'sdg_10_10'. Da bruker vi funksjonen get_data_df
med tabellkoden som argument:
gdp_data = eurostat.get_data_df('sdg_10_10')
gdp_data
Tabellen inneholder imidlertid endel data vi ikke ønsker å ha med. For det første ønsker vi kun å ta med tilfellene der feltet "na_item" er lik 'EXP_PPS_EU27_2020_HAB', for det andre er her dataene til hvert enkelt land ligger. Dette gjør vi på samme måte som over med listen gdp_data['na_item']=='EXP_PPS_EU27_2020_HAB'
, som er sann når "na_item" er lik 'EXP_PPS_EU27_2020_HAB'. Setter vi inn denne listen i klammeparentes etter gdp_data
får vi en filtrert liste:
gdp_data = gdp_data[gdp_data['na_item']=='EXP_PPS_EU27_2020_HAB']
gdp_data
Videre ønsker vi å fjerne de definisjonene av EU vi ikke trenger, som er 'EA18','EA19','EU27_2007','EU27_2020','EA18' og 'EA18'. Vi bruker samme teknikk, vi lager en liste som inneholder True
og False
, avhengig om en betingelse er oppfylt.
Feltet vi nå skal filtrere ut fra er "geo\time", som innholder landkodene. Siden vi ikke bare skal sjekke om én streng er i denne kolonnen, men om flere mulige kandidater er det, kan vi ikke bruke "list comprehension" som over. Vi bruker i stedet en pandas-funksjon isin
(is in), som tar en liste med "ting" vi ikke vil ha i feltet som argument.
unwanted=gdp_data['geo\\TIME_PERIOD'].isin(['EA18','EA19','EU27_2007','EU27_2020','EA18','EA18'])
Men vi vil ikke ha unwanted
, så vi tar kun med tilfellene der unwanted==False
. I tillegg endrer vi kollonnenavnet for land til noe mer gjenkjennelig.
gdp_data_fltrd = gdp_data[unwanted==False]
gdp_data_fltrd = gdp_data_fltrd.rename(columns={'geo\\TIME_PERIOD':'country'})
La oss se på utviklingen i BNP justert for kjøpekraft siden Angela Merkel tiltrådte som forbundskansler i 2005. Vi velger ut kolonnene 2005 til 2020, i tillegg til kolonnen 'geo\time' med landkodene, som skal stå først. Vi gjør det ved å lage en liste med de kolonnene vi ønsker å hente ut fra gdp_data
. Vi lager da to lister som vi slår sammen:
Liste 1: Vi starter med navnet til første kolonne 'geo\time', som vi lager om til en liste med ett element ved hjelp av klammeparenteser: ['geo\\time']
.
Liste 2: Vi bruker så den innebygde range()
til å generere fra 2005 til 2020, og konverterer resultatet til en liste: list(range(2005,2021)
.
Summen av disse to listene blir da en liste med 'geo\time' først og så årstallene.
gdp_data_fltrd
chosen_columns=['country']+[str(i) for i in range(2005,2021)]
chosen_columns
Vi kan nå velge ut kun disse kolonnene ved å velge samtlige rader fra gdp_data.loc, og kolonnene angitt av listen:
gdp_data_fltrd2 = gdp_data_fltrd[chosen_columns]
Så ønsker vi videre å kategorisere observasjonene ut fra landkode, og ikke rekkefølge. Vi setter derfor 'geo\time' som indeks:
gdp_data_fltrd2=gdp_data_fltrd2.set_index('country')
gdp_data_fltrd2
Vi skal nå "rebasere" tallene slik at alle starter på 100. Vi må først sørge for at hvert årstall er en observasjon/rad, og ikke en kolonne. Vi gjør det med pandasfunksjonen transpose()
. Den bytter om på rader og kolonner:
gdp_data_fltrd3=gdp_data_fltrd2.transpose()
gdp_data_fltrd3
Vi kan nå "rebasere". Vi gjør det ved å bruke pandasfunksjonen div()
, som deler alle observasjonene på argumentet. Vi ønsker å dele på første rad (år 2005), som er gdp_data.iloc[0]
:
gdp_data_fltrd4=100*gdp_data_fltrd3.div(gdp_data_fltrd3.iloc[0])
gdp_data_fltrd4
Vi kan nå plotte resultatet med plot()
-funksjonen i pandas. Vi velger gjennomsiktighet 0.3 og figurstørrelse 20x10. Etikettene plasseres øverst til venstre, uten ramme og i fire kolonner:
ax=gdp_data_fltrd4.plot(alpha=0.3,figsize=(20, 10))
ax.legend(loc='upper left',frameon=False,ncol=4)
Som nevnt har vi valgt perioden fra 2005 fordi Angela Merkel tiltrådte da. Det kan derfor være en god idé å utheve Tysklands kurve. Vi skal gjøre det ved å øke linjevidden til 5 og ta bort gjennomsiktighet (alpha=1). For at det skal være enkelt å også utheve andre land, lager vi en funksjon som uthever land med et bestemt symbol. ax
representerer grafen, og må også være et argument, slik at funksjonen kan operere på den. Her er funksjonen:
def thick_line(symbol,ax):
labels=[i._label for i in ax.lines] #makes a list of symbols in the order of lines in the axis object
i=labels.index(symbol) #identifies at which position symbol is located
ax.lines[i].set_linewidth(5) #sets the line with of the line at position i to 5
ax.lines[i].set_alpha(1) #removes transparancy of the line at position i to 5
Funksjonen gjør følgende:
symbol
befinner segVi kan nå plotte grafen, etter å ha uthevet linjene til 'DE' (Tyskland) 'NO' (Norge) og 'EU28' (EU inkludert Storbritannia):
thick_line('DE',ax)
thick_line('NO',ax)
thick_line('EU28',ax)
ax.legend(loc='upper left',frameon=False,ncol=4)
ax.figure
scatter()
-funksjonen for å se om det er en sammenheng mellom avkastningene.
animal
. Hvorfor?
or edu>10
fra koden? Forklar.
toc[['Electricity' in i for i in toc['title']]]
og finn koden i feltet 'code' til tabellen 'Electricity prices for household consumers - bi-annual data (from 2007 onwards)'. el_data = el_data[((el_data['tax']=='I_TAX')
&(el_data['currency']=='EUR')
&(el_data['nrg_cons']=='KWH5000-14999'))]